/*
 * This file is part of the <a href="http://code.google.com/p/pux">PUX Framework</a>
 *
 * Copyright 2008 by <a href="mailto:jfendler@polytechnic.edu.na">Jens Fendler</a>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package de.teamskill.pux.script;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.ListIterator;
import java.util.Map;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import de.teamskill.pux.engine.AbstractNode;
import de.teamskill.pux.engine.AbstractXMLNode;
import de.teamskill.pux.engine.CustomizableInputPorts;
import de.teamskill.pux.engine.CustomizableOutputPorts;
import de.teamskill.pux.engine.InputPort;
import de.teamskill.pux.engine.Network;
import de.teamskill.pux.engine.OutputPort;

/**
 * NetworkBuilder
 * 
 * @author <a href="mailto:jfendler@polytechnic.edu.na">Jens Fendler</a>
 * 
 */
public class NetworkBuilder implements PUXScriptConstants {

	/**
	 * scriptInputStream
	 */
	private InputStream scriptInputStream;

	/**
	 * parameters
	 */
	private Map<String, String> parameters;

	/**
	 * network
	 */
	private Network network;

	/**
	 * @throws FileNotFoundException
	 *             thrown if the specified file was not found
	 * 
	 */
	public NetworkBuilder(File scriptFile) throws FileNotFoundException {
		this(new FileInputStream(scriptFile), new HashMap<String, String>());
	}

	/**
	 * NetworkBuilder Constructor
	 * 
	 * @param fileInputStream
	 */
	public NetworkBuilder(InputStream scriptInputStream) {
		this(scriptInputStream, new HashMap<String, String>());
	}

	/**
	 * NetworkBuilder Constructor
	 * 
	 * @param scriptFileName
	 * @throws FileNotFoundException
	 *             thrown if the file was not found
	 */
	public NetworkBuilder(String scriptFileName) throws FileNotFoundException {
		this(new FileInputStream(scriptFileName), new HashMap<String, String>());
	}

	/**
	 * NetworkBuilder Constructor
	 * 
	 * @param scriptFile
	 * @param parameters
	 * @throws FileNotFoundException
	 */
	public NetworkBuilder(File scriptFile, Map<String, String> parameters) throws FileNotFoundException {
		this(new FileInputStream(scriptFile), parameters);
	}

	/**
	 * NetworkBuilder Constructor
	 * 
	 * @param scriptFileName
	 * @param parameters
	 * @throws FileNotFoundException
	 */
	public NetworkBuilder(String scriptFileName, Map<String, String> parameters) throws FileNotFoundException {
		this(new FileInputStream(scriptFileName), parameters);
	}

	/**
	 * NetworkBuilder Constructor
	 * 
	 * @param scriptInputStream
	 * @param parameters
	 */
	public NetworkBuilder(InputStream scriptInputStream, Map<String, String> parameters) {
		this.scriptInputStream = scriptInputStream;
		this.parameters = parameters;
	}

	/**
	 * @param fis
	 * @return
	 * @throws JDOMException
	 * @throws IOException
	 */
	public Network createNetwork() throws JDOMException, IOException {
		if (scriptInputStream == null) {
			throw new IllegalStateException("No script InputStream has been set.");
		}
		network = new Network();
		SAXBuilder builder = new SAXBuilder(false);
		Document doc = builder.build(scriptInputStream);
		Element networkNode = doc.getRootElement();
		if (!ELEMENT_NETWORK.equals(networkNode.getName())) {
			throw new JDOMException("Not a PUX network script");
		}
		parseParameters(networkNode);

		addNodes(networkNode);
		addConnections(networkNode);

		return network;
	}

	/**
	 * parseParameters
	 * 
	 * @param networkNode
	 */
	@SuppressWarnings("unchecked")
	protected void parseParameters(Element networkNode) {
		for (ListIterator<Element> li = networkNode.getChildren(ELEMENT_PARAMETER).listIterator(); li
				.hasNext();) {
			Element parameterElement = li.next();
			String key = evaluateParameters(parameterElement.getAttributeValue(ATTR_KEY));
			String value = evaluateParameters(parameterElement.getAttributeValue(ATTR_VALUE));
			parameters.put(key, value);
		}
	}

	/**
	 * evaluateParameters
	 * 
	 * @param rawString
	 * @return
	 */
	protected String evaluateParameters(String rawString) {
		if (rawString != null) {
			for (String pName : parameters.keySet()) {
				rawString = rawString.replaceAll("\\$\\$" + pName + "\\$\\$", parameters.get(pName));
			}
		}
		return rawString;
	}

	/**
	 * addConnections
	 * 
	 * @param n
	 * @param networkNode
	 */
	@SuppressWarnings("unchecked")
	protected void addConnections(Element networkNode) {
		for (ListIterator<Element> li = networkNode.getChildren(ELEMENT_CONNECTION).listIterator(); li
				.hasNext();) {
			Element connectionElement = li.next();
			AbstractNode sourceNode = network.getNodes().get(
					evaluateParameters(connectionElement.getAttributeValue(ATTR_SOURCENODE)));
			AbstractNode targetNode = network.getNodes().get(
					evaluateParameters(connectionElement.getAttributeValue(ATTR_TARGETNODE)));
			OutputPort sourcePort = sourceNode.getOutputPort(evaluateParameters(connectionElement
					.getAttributeValue(ATTR_SOURCEPORT)));
			InputPort targetPort = targetNode.getInputPort(evaluateParameters(connectionElement
					.getAttributeValue(ATTR_TARGETPORT)));
			network.connect(sourcePort, targetPort);
		}
	}

	/**
	 * addNodes
	 * 
	 * @param n
	 * @param networkNode
	 * @throws IOException
	 */
	@SuppressWarnings("unchecked")
	protected void addNodes(Element networkNode) throws IOException {
		for (ListIterator<Element> li = networkNode.getChildren(ELEMENT_NODE).listIterator(); li.hasNext();) {
			Element nodeElement = li.next();
			try {
				String nodeName = evaluateParameters(nodeElement.getAttributeValue(ATTR_ID));

				boolean continuous = false;
				boolean start = false;

				if (nodeElement.getAttribute(ATTR_CONTINUOUS) != null) {
					continuous = Boolean.parseBoolean(evaluateParameters(nodeElement
							.getAttributeValue(ATTR_CONTINUOUS)));
				}

				if (nodeElement.getAttribute(ATTR_START) != null) {
					start = Boolean
							.parseBoolean(evaluateParameters(nodeElement.getAttributeValue(ATTR_START)));
				}

				// create the node and add it to the network
				AbstractNode node = (AbstractNode) Class.forName(
						evaluateParameters(nodeElement.getAttributeValue(ATTR_CLASS))).newInstance();

				Element setupElement = nodeElement.getChild(ELEMENT_SETUP);
				if (setupElement != null) {
					if (node instanceof AbstractXMLNode) {
						// pass the DOM element to the node for setup
						node.setup(setupElement);
					} else {
						// attempt plain-text setup
						node.setup(setupElement.getText());
					}
				}
				node.setNodeName(nodeName);
				node.setContinuous(continuous);
				if (start) {
					node.makeStartupNode();
				}

				if (node instanceof CustomizableInputPorts) {
					// add custom input ports to the node
					for (ListIterator ipe = nodeElement.getChildren(ELEMENT_INPUT).listIterator(); ipe
							.hasNext();) {
						Element ipElement = (Element) ipe.next();
						addCustomInputPort(node, ipElement);
					}
				}

				if (node instanceof CustomizableOutputPorts) {
					// add custom output ports to the node
					for (ListIterator ope = nodeElement.getChildren(ELEMENT_OUTPUT).listIterator(); ope
							.hasNext();) {
						Element opElement = (Element) ope.next();
						addCustomOutputPort(node, opElement);
					}
				}

				network.addNode(node);

				// configure the node (if applicable)
				for (ListIterator<Element> li2 = nodeElement.getChildren(ELEMENT_CONFIG).listIterator(); li2
						.hasNext();) {
					Element configElement = li2.next();
					String configPort = evaluateParameters(configElement.getAttributeValue(ATTR_PORT));
					String value = evaluateParameters(configElement.getTextNormalize());
					node.configure(configPort, value);
				}

			} catch (Exception e) {
				throw new IOException("Failed to load node class: " + e.getLocalizedMessage());
			}
		}
	}

	/**
	 * addCustomOutputPort
	 * 
	 * @param node
	 * @param opElement
	 */
	private void addCustomOutputPort(AbstractNode node, Element opElement) {
		String opPort = opElement.getAttributeValue(ATTR_PORT);
		if (opPort == null) {
			throw new IllegalArgumentException("Missing port attribute for output port on node "
					+ node.getNodeName());
		}
		String opTypeString = opElement.getAttributeValue(ATTR_TYPE);
		Class<?> opType = Object.class;
		if (opType != null) {
			try {
				opType = Class.forName(opTypeString);
			} catch (ClassNotFoundException cnfe) {
				throw new IllegalArgumentException("Invalid type (" + opTypeString
						+ ") for output port on node " + node.getNodeName());
			}
		}
		OutputPort op = new OutputPort(opPort, node, opType);
		node.addOutputPort(op);
	}

	/**
	 * addCustomInputPort
	 * 
	 * @param node
	 * @param ipElement
	 */
	private void addCustomInputPort(AbstractNode node, Element ipElement) {
		String ipPort = ipElement.getAttributeValue(ATTR_PORT);
		if (ipPort == null) {
			throw new IllegalArgumentException("Missing port attribute for input port on node "
					+ node.getNodeName());
		}
		String ipRequiredString = ipElement.getAttributeValue(ATTR_REQUIRED);
		boolean ipRequired = ipRequiredString != null && ipRequiredString.toLowerCase().equals("true");
		String ipTypeString = ipElement.getAttributeValue(ATTR_TYPE);
		Class<?> ipType = Object.class;
		if (ipType != null) {
			try {
				ipType = Class.forName(ipTypeString);
			} catch (ClassNotFoundException cnfe) {
				throw new IllegalArgumentException("Invalid type (" + ipTypeString
						+ ") for input port on node " + node.getNodeName());
			}
		}
		InputPort ip = new InputPort(ipPort, node, ipRequired, ipType);
		node.addInputPort(ip);
	}

	/**
	 * JavaBean-style getter-method for scriptInputStream
	 * 
	 * @return the scriptInputStream
	 */
	public InputStream getScriptInputStream() {
		return scriptInputStream;
	}

	/**
	 * /** JavaBean-style setter-method for scriptInputStream
	 * 
	 * @param scriptInputStream
	 *            the scriptInputStream to set
	 */
	public void setScriptInputStream(InputStream scriptInputStream) {
		this.scriptInputStream = scriptInputStream;
	}

	/**
	 * JavaBean-style getter-method for parameters
	 * 
	 * @return the parameters
	 */
	public Map<String, String> getParameters() {
		return parameters;
	}

	/**
	 * /** JavaBean-style setter-method for parameters
	 * 
	 * @param parameters
	 *            the parameters to set
	 */
	public void setParameters(Map<String, String> parameters) {
		this.parameters = parameters;
	}

	/**
	 * JavaBean-style getter-method for network
	 * 
	 * @return the network
	 */
	public Network getNetwork() {
		return network;
	}

	/**
	 * /** JavaBean-style setter-method for network
	 * 
	 * @param network
	 *            the network to set
	 */
	public void setNetwork(Network network) {
		this.network = network;
	}

}
